package docker

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
)

const _registry = "registry-1.docker.io"
const _authUrl = "https://auth.docker.io/token"
const _regService = "registry.docker.io"

type Layer struct {
	Digest string
	Urls   []string
}

type Info struct {
	Layers []Layer `json:"layers"`
	Config struct {
		Digest string `json:"digest"`
	} `json:"config"`
}

type LayerInfo struct {
	Id              string    `json:"id"`
	Parent          string    `json:"parent"`
	Created         time.Time `json:"created"`
	ContainerConfig struct {
		Hostname     string
		Domainname   string
		User         string
		AttachStdin  bool
		AttachStdout bool
		AttachStderr bool
		Tty          bool
		OpenStdin    bool
		StdinOnce    bool
		Env          []string
		CMd          []string
		Image        string
		Volumes      map[string]interface{}
		WorkingDir   string
		Entrypoint   []string
		OnBuild      []string
		Labels       map[string]interface{}
	} `json:"container_config"`
}

type PackageConfig struct {
	Config   string
	RepoTags []string
	Layers   []string
}

func Install(d, tag string) (err error) {
	var authUrl = _authUrl
	var regService = _regService
	resp, err := http.Get(fmt.Sprintf("https://%s/v2/", _registry))
	if err == nil {
		if resp.StatusCode == 401 {
			//Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
			var hAuths = strings.Split(resp.Header.Get("Www-Authenticate"), "\"")
			if len(hAuths) > 1 {
				authUrl = hAuths[1]
			}
			if len(hAuths) > 3 {
				regService = hAuths[3]
			} else {
				regService = ""
			}
		}
		resp.Body.Close()
		var accessToken string
		fmt.Println("reg_service", regService)
		accessToken, err = getAuthHead("application/vnd.docker.distribution.manifest.v2+json", authUrl, regService, d)
		if err == nil {
			var req *http.Request

			var url = fmt.Sprintf("https://%s/v2/%s/manifests/%s", _registry, d, tag)
			req, err = http.NewRequest("GET", url, nil)
			if err == nil {
				req.Header.Add("Authorization", "Bearer "+accessToken)
				req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")

				var authHeader = req.Header

				resp, err = http.DefaultClient.Do(req)
				if resp.StatusCode != 200 {
					switch resp.StatusCode {
					case 401:
						fmt.Printf("[-] Cannot fetch manifest for %s [HTTP %d] with access_token %s", d, resp.StatusCode, accessToken)
					case 404:
						fmt.Printf("[-] Cannot fetch manifest for %s [HTTP %d] with url %s", d, resp.StatusCode, url)
						resp.Body.Close()
						req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
						resp, err = http.DefaultClient.Do(req)
						bts, er := ioutil.ReadAll(resp.Body)
						fmt.Println(string(bts), er)
					}
					//TODO
					os.Exit(1)
				} else {
					var info Info
					err = json.NewDecoder(resp.Body).Decode(&info)
					resp.Body.Close()
					if err == nil {
						var tmpDir = fmt.Sprintf("tmp_%s_%s", d, tag)
						err = os.MkdirAll(tmpDir, 0777)
						if err == nil {
							if _, e := os.Stat(filepath.Join(tmpDir, "repositories")); e == nil {
								logrus.Info(tmpDir, "is downloaded,use dir as cache")
							} else {
								req, err = http.NewRequest("GET", fmt.Sprintf("https://%s/v2/%s/blobs/%s", _registry, d, info.Config.Digest), nil)
								if err == nil {
									req.Header = authHeader
									resp, err = http.DefaultClient.Do(req)
									if err == nil {
										var dest *os.File
										dest, err = os.OpenFile(filepath.Join(tmpDir, info.Config.Digest[7:]+".json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
										if err == nil {
											var bts []byte
											bts, err = ioutil.ReadAll(resp.Body)
											var lastLayerInfo LayerInfo
											err = json.Unmarshal(bts, &lastLayerInfo)
											resp.Body.Close()

											var config []PackageConfig
											config = append(config, PackageConfig{
												Config:   info.Config.Digest[7:] + ".json",
												RepoTags: []string{d + ":" + tag},
											})
											if err == nil {
												_, err = io.Copy(dest, bytes.NewReader(bts))
												dest.Close()
												if err == nil {
													parentid := ""
													var fakeLayerId string
													for n, layer := range info.Layers {
														fmt.Println("handle layer", n)
														namer := sha256.New()
														namer.Write([]byte(parentid + "\n" + layer.Digest + "\n"))
														fakeLayerId = hex.EncodeToString(namer.Sum(nil))
														layerDirName := filepath.Join(tmpDir, fakeLayerId)
														err = os.Mkdir(layerDirName, 0777)
														if err == nil {
															err = ioutil.WriteFile(filepath.Join(layerDirName, "VERSION"), []byte("1.0"), 0666)
															if err == nil {
																req, err = http.NewRequest("GET", fmt.Sprintf("https://%s/v2/%s/blobs/%s", _registry, d, layer.Digest), nil)
																if err == nil {
																	req.Header = authHeader
																	req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
																	resp, err = http.DefaultClient.Do(req)
																	if err == nil {
																		if resp.StatusCode != 200 {
																			resp.Body.Close()
																			req, err = http.NewRequest("GET", layer.Urls[0], nil)
																			if err == nil {
																				req.Header = authHeader
																				req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
																				resp, err = http.DefaultClient.Do(req)
																				if err == nil {
																					if resp.StatusCode != 200 {
																						err = fmt.Errorf("download from customized url fail for layer[%d]", n)
																						goto response
																					}
																				}
																			}
																		}
																	}
																	var dst *os.File
																	dst, err = os.OpenFile(filepath.Join(layerDirName, "layer.tar"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
																	if err == nil {
																		var greader *gzip.Reader
																		greader, err = gzip.NewReader(resp.Body)
																		if err == nil {
																			_, err = io.Copy(dst, greader)
																			if err == nil {
																				var layerInfo LayerInfo
																				if n == len(info.Layers)-1 {
																					layerInfo = lastLayerInfo
																				}
																				layerInfo.Id = fakeLayerId
																				if parentid != "" {
																					layerInfo.Parent = parentid
																				}
																				parentid = fakeLayerId
																				var jsonFile *os.File
																				jsonFile, err = os.OpenFile(filepath.Join(layerDirName, "json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
																				if err == nil {
																					err = json.NewEncoder(jsonFile).Encode(&layerInfo)
																				}
																			}
																		}
																	}
																	if err != nil {
																		goto response
																	} else {
																		config[0].Layers = append(config[0].Layers, fakeLayerId+"/layer.tar")
																	}
																}
															}
														}
													}
													var manifest *os.File
													manifest, err = os.OpenFile(filepath.Join(tmpDir, "manifest.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
													if err == nil {
														err = json.NewEncoder(manifest).Encode(&config)
														if err == nil {
															manifest.Close()
															var repositories = make(map[string]interface{})
															repositories[d] = map[string]string{
																tag: fakeLayerId,
															}
															var rFile *os.File
															rFile, err = os.OpenFile(filepath.Join(tmpDir, "repositories"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
															if err == nil {
																err = json.NewEncoder(rFile).Encode(&repositories)
																goto maketar
															}
														}

													}
												}
											}
										}
									}
								}

							}
						maketar:
							if err == nil {
								err = writeDirToTar(tmpDir, tmpDir+"-img.tar")
								if err == nil {
									fmt.Println("write tar success", tmpDir+"-img.tar")
								}
							}
						}
					}
				}
			}
		}
	}
response:
	return
}

func getAuthHead(u, a, r, d string) (string, error) {
	resp, err := http.Get(fmt.Sprintf("%s?service=%s&scope=repository:%s:pull", a, r, d))
	defer resp.Body.Close()
	if err == nil {
		var results map[string]interface{}
		err = json.NewDecoder(resp.Body).Decode(&results)
		logrus.Info(results)
		if err == nil {
			return results["access_token"].(string), nil
		}
	}
	return "", err
}

func writeDirToTar(sourcedir, destinationfile string) error {
	// create tar file
	tarfile, err := os.Create(destinationfile)
	tw := tar.NewWriter(tarfile)
	if err == nil {

		defer tarfile.Close()

		// get list of files
		return filepath.Walk(sourcedir, func(path string, info os.FileInfo, err error) error {
			relPath, err := filepath.Rel(sourcedir, path)
			if err == nil && relPath != "." {
				header, err := tar.FileInfoHeader(info, path)
				if err != nil {
					return err
				}

				// must provide real name
				// (see https://golang.org/src/archive/tar/common.go?#L626)
				header.Name = filepath.ToSlash(relPath)

				// write header
				if err := tw.WriteHeader(header); err != nil {
					return err
				}
				// if not a dir, write file content
				if !info.IsDir() {
					data, err := os.Open(path)
					if err != nil {
						return err
					}
					if _, err := io.Copy(tw, data); err != nil {
						return err
					}
				}
				return nil
			}
			return err
		})

	}
	return err
}
